/**
 * Copyright (c) 2021-2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "inline_call_intrinsics.h"
#include "plugins/ecmascript/runtime/js_tagged_value.h"
#include "optimizer/ir/basicblock.h"
#include "optimizer/analysis/dominators_tree.h"
#include "optimizer/analysis/loop_analyzer.h"
#include "runtime/include/coretypes/tagged_value.h"

namespace ark::compiler {

bool InlineCallIntrinsics::RunImpl()
{
    bool success = false;
    for (auto *bb : GetGraph()->GetVectorBlocks()) {
        if (bb == nullptr) {
            continue;
        }

        for (auto inst : bb->InstsSafeReverse()) {
            if (!inst->IsIntrinsic()) {
                continue;
            }
            auto intrinInst = inst->CastToIntrinsic();
            success |= Expand(intrinInst);
        }
    }
    return success;
}

bool InlineCallIntrinsics::Expand(IntrinsicInst *inst)
{
    auto id = inst->GetIntrinsicId();
    switch (id) {
        case RuntimeInterface::IntrinsicId::INTRINSIC_NEWOBJ_DYNRANGE:
        case RuntimeInterface::IntrinsicId::INTRINSIC_NEWOBJ_DYNRANGE_HANDLED:
            return ExpandNewObjDynRange(inst);
        default:
            return false;
    }
}

Inst *InlineCallIntrinsics::CreateAllocDynObjIntrinsic(Inst *ctor, SaveStateInst *ss, uint32_t pc)
{
    auto allocObj =
        GetGraph()->CreateInstIntrinsic(DataType::ANY, pc, RuntimeInterface::IntrinsicId::INTRINSIC_ALLOC_DYN_OBJECT);
    allocObj->SetInputs(GetGraph()->GetAllocator(), {{ctor, DataType::ANY}, {ss, DataType::NO_TYPE}});
    return allocObj;
}

Inst *InlineCallIntrinsics::NewObjCreateConstructorCall(Inst *origCall, Inst *allocObj, SaveStateInst *ss, uint32_t pc)
{
    auto callCtor = GetGraph()->CreateInstCallDynamic(DataType::ANY, pc);
    callCtor->ReserveInputs(origCall->GetInputsCount() + 1);
    callCtor->AllocateInputTypes(GetGraph()->GetAllocator(), origCall->GetInputsCount() + 1);
    callCtor->AppendInputs({{origCall->GetInput(1).GetInst(), DataType::ANY},
                            {origCall->GetInput(2).GetInst(), DataType::ANY},
                            {allocObj, DataType::ANY}});
    for (size_t i = 3; i < origCall->GetInputsCount() - 1; ++i) {
        callCtor->AppendInput(origCall->GetInput(i).GetInst(), origCall->GetInputType(i));
    }
    callCtor->AppendInput(ss, DataType::NO_TYPE);
    return callCtor;
}

void InlineCallIntrinsics::NewObjFillCurrBlock(InstAppender *appender, Inst *allocObj, uint32_t pc)
{
    auto cmp = GetGraph()->CreateInstCompareAnyType(pc, allocObj, AnyBaseType::ECMASCRIPT_NULL_TYPE);
    // if alloc returned null -> jump into a slow path
    auto ifimm = GetGraph()->CreateInstIfImm(DataType::BOOL, pc, cmp, 0, DataType::BOOL, ConditionCode::CC_NE);
    appender->Append({cmp, ifimm});
}

void InlineCallIntrinsics::NewObjFillSlowPathBlock(InstAppender *appender, Inst *origAlloc)
{
    auto currBlock = origAlloc->GetBasicBlock();
    auto ss = origAlloc->CastToIntrinsic()->GetSaveState();
    auto ssCopy = static_cast<SaveStateInst *>(ss->Clone(GetGraph()));
    for (size_t inputIdx = 0; inputIdx < ss->GetInputsCount(); ++inputIdx) {
        ssCopy->AppendInput(ss->GetInput(inputIdx));
        ssCopy->SetVirtualRegister(inputIdx, ss->GetVirtualRegister(inputIdx));
    }
    origAlloc->SetSaveState(ssCopy);
    // move original intrinsic and its SaveState into a slow path
    currBlock->EraseInst(origAlloc, true);
    appender->Append({ssCopy, origAlloc});
}

Inst *InlineCallIntrinsics::NewObjFillCallConstructorBlock(InstAppender *appender, Inst *origAlloc, Inst *allocObj,
                                                           uint32_t pc)
{
    auto ss = origAlloc->GetSaveState();
    auto ssCopy = static_cast<SaveStateInst *>(ss->Clone(GetGraph()));
    for (size_t inputIdx = 0; inputIdx < ss->GetInputsCount(); ++inputIdx) {
        ssCopy->AppendInput(ss->GetInput(inputIdx));
        ssCopy->SetVirtualRegister(inputIdx, ss->GetVirtualRegister(inputIdx));
    }
    ssCopy->AppendBridge(allocObj);
    auto callCtor = NewObjCreateConstructorCall(origAlloc, allocObj, ssCopy, pc);

    appender->Append({ssCopy, callCtor});

    return callCtor;
}

Inst *InlineCallIntrinsics::NewObjResolveCtorResult(InstAppender *appender, Inst *origAlloc, Inst *allocObj,
                                                    Inst *callCtor, uint32_t pc)
{
    auto ss = callCtor->GetSaveState();
    auto ssCopy2 = static_cast<SaveStateInst *>(ss->Clone(GetGraph()));
    for (size_t inputIdx = 0; inputIdx < ss->GetInputsCount(); ++inputIdx) {
        ssCopy2->AppendInput(ss->GetInput(inputIdx));
        ssCopy2->SetVirtualRegister(inputIdx, ss->GetVirtualRegister(inputIdx));
    }
    ssCopy2->AppendBridge(callCtor);

    auto resolveResult = GetGraph()->CreateInstIntrinsic(DataType::ANY, pc,
                                                         RuntimeInterface::IntrinsicId::INTRINSIC_RESOLVE_ALLOC_RESULT);
    resolveResult->SetInputs(GetGraph()->GetAllocator(), {{origAlloc->GetInput(1).GetInst(), DataType::ANY},
                                                          {allocObj, DataType::ANY},
                                                          {callCtor, DataType::ANY},
                                                          {ssCopy2, DataType::NO_TYPE}});
    resolveResult->SetInlined(true);

    appender->Append({ssCopy2, resolveResult});

    return resolveResult;
}

/*
 * Expands NewObjDynRange intrinsics into a fast path consisting of memory
 * allocation via AllocDynObject intrinsic, constructor's call, resolution
 * of a result via ResolveAllocResult intrinsic and a slow path consisting of
 * NewObjDynRange intrinsic call.
 *
 * Expanded IR has following shape:
 *
 *     allocated_value := AllocDynObject
 *        if allocated_value is null
 *        /                         \
 *       /                           \
 * slow_path_res := NewObjDynRange    |
 *      |         ctor_res := call Ctor(allocated_value)
 *      |         fast_path_res := ResolveAllocResult(Ctor, allocated_value, ctor_res)
 *       \                           /
 *        \_________________________/
 *                    |
 *    res := Phi(slow_path_res, fast_path_res)
 *
 * Such expansion improves performance of object allocation by using specialized
 * stubs outperforming generic runtime code and allows inlining of a constructor.
 *
 * Depending on profiling info shape of the emitted IR may change by pruning one of the branches.
 */
bool InlineCallIntrinsics::ExpandNewObjDynRange(IntrinsicInst *inst)
{
    Inst *ctorInst = inst->GetInput(1).GetInst();
    Inst *newTargetInst = inst->GetInput(2).GetInst();

    if (ctorInst != newTargetInst) {
        return false;
    }

    auto runtime = GetGraph()->GetRuntime();
    auto kind = ark::profiling::CallKind::UNKNOWN;
    if (g_options.IsCompilerEcmaNewobjProfiling()) {
        functions_.clear();
        kind = runtime->GetCallProfile(GetGraph()->GetMethod(), inst->GetPc(), &functions_, GetGraph()->IsAotMode());
    }
    RuntimeInterface::NewObjDynInfo expansionInfo {RuntimeInterface::NewObjDynInfo::AllocatorType::DEFAULT,
                                                   RuntimeInterface::NewObjDynInfo::ResolverType::RESOLVE};
    if (kind == ark::profiling::CallKind::MONOMORPHIC) {
        expansionInfo = runtime->GetNewObjDynInfo(functions_.front());
    }

    switch (expansionInfo.allocation) {
        // allocation always require slow path, so it doesn't make sense to expand the intrinsic
        case RuntimeInterface::NewObjDynInfo::AllocatorType::SLOW_PATH:
            assert(expansionInfo.resolution == RuntimeInterface::NewObjDynInfo::ResolverType::RESOLVE);
            return false;
        // emit simplified fast path, but only for monomorphic call sites
        case RuntimeInterface::NewObjDynInfo::AllocatorType::UNDEFINED:
        case RuntimeInterface::NewObjDynInfo::AllocatorType::ALLOC_OBJ:
            if (kind == ark::profiling::CallKind::MONOMORPHIC) {
                ExpandNewObjDynFastPath(inst, expansionInfo, functions_.front());
                return true;
            } else {
                ExpandNewObjDynDefault(inst);
                return true;
            }
        default:
            ExpandNewObjDynDefault(inst);
            return true;
    }

    return false;
}

void InlineCallIntrinsics::ExpandNewObjDynDefault(Inst *inst)
{
    auto currBlock = inst->GetBasicBlock();
    auto succBlock = currBlock->SplitBlockAfterInstruction(inst, false);
    auto pc = inst->GetPc();

    auto slowPathBlock = GetGraph()->CreateEmptyBlock(currBlock);
    auto callCtorBlock = GetGraph()->CreateEmptyBlock(currBlock);

    InstAppender currBlockAppender {currBlock};
    auto allocObj = CreateAllocDynObjIntrinsic(inst->GetInput(1).GetInst(), inst->GetSaveState(), pc);
    currBlockAppender.Append({allocObj});
    NewObjFillCurrBlock(&currBlockAppender, allocObj, pc);

    InstAppender slowPathBlockAppender {slowPathBlock};
    NewObjFillSlowPathBlock(&slowPathBlockAppender, inst);

    InstAppender callCtorBlockAppender {callCtorBlock};
    auto ctor = NewObjFillCallConstructorBlock(&callCtorBlockAppender, inst, allocObj, pc);
    auto ctorRes = NewObjResolveCtorResult(&callCtorBlockAppender, inst, allocObj, ctor, pc);

    currBlock->AddSucc(slowPathBlock);
    currBlock->AddSucc(callCtorBlock);
    slowPathBlock->AddSucc(succBlock);
    callCtorBlock->AddSucc(succBlock);

    auto phi = GetGraph()->CreateInstPhi(DataType::ANY, pc);
    inst->ReplaceUsers(phi);
    phi->AppendInput(inst);
    phi->AppendInput(ctorRes);
    succBlock->AppendPhi(phi);

    GetGraph()->InvalidateAnalysis<LoopAnalyzer>();
    GetGraph()->InvalidateAnalysis<DominatorsTree>();
    InvalidateBlocksOrderAnalyzes(GetGraph());
}

void InlineCallIntrinsics::ExpandNewObjDynFastPath(Inst *inst, RuntimeInterface::NewObjDynInfo info, uintptr_t target)
{
    auto currBlock = inst->GetBasicBlock();
    auto pc = inst->GetPc();

    BuildGuard(inst, target);

    Inst *allocObj;
    InstAppender appender {currBlock, inst};
    if (info.allocation == RuntimeInterface::NewObjDynInfo::AllocatorType::ALLOC_OBJ) {
        allocObj = CreateAllocDynObjIntrinsic(inst->GetInput(1).GetInst(), inst->GetSaveState(), pc);
        appender.Append({allocObj});
    } else {
        ASSERT(info.allocation == RuntimeInterface::NewObjDynInfo::AllocatorType::UNDEFINED);
        allocObj = GetGraph()->FindOrCreateConstant(DataType::Any(ark::coretypes::TaggedValue::VALUE_UNDEFINED));
    }

    auto ctorRes = NewObjFillCallConstructorBlock(&appender, inst, allocObj, pc);

    Inst *res = ctorRes;

    bool needResolver = info.resolution == RuntimeInterface::NewObjDynInfo::ResolverType::RESOLVE;
    if (needResolver) {
        res = NewObjResolveCtorResult(&appender, inst, allocObj, ctorRes, pc);
    }
    inst->ReplaceUsers(res);
    currBlock->RemoveInst(inst);
}

void InlineCallIntrinsics::BuildGuard(Inst *inst, uintptr_t target)
{
    auto pc = inst->GetPc();
    auto loadFunction = GetGraph()->CreateInstFunctionImmediate(DataType::ANY, pc, target);
    auto cmpInst = GetGraph()->CreateInstCompare(DataType::BOOL, pc, loadFunction, inst->GetInput(1).GetInst(),
                                                 DataType::ANY, ConditionCode::CC_NE);
    auto deoptInst = GetGraph()->CreateInstDeoptimizeIf(pc, cmpInst, inst->GetSaveState(), DeoptimizeType::INLINE_DYN);

    auto saveState = inst->GetSaveState();
    ASSERT(saveState);
    saveState->InsertBefore(loadFunction);
    saveState->AppendBridge(loadFunction);
    inst->InsertBefore(cmpInst);
    inst->InsertBefore(deoptInst);
}

}  // namespace ark::compiler
